/*  
 *  Copyright 2006 Paul Jack.
 *
 *  This file is part of the Dex compiler suite. 
 *  
 *  Dex is free software; you can redistribute it and/or modify it
 *  under the terms of the GNU General Public License as published by the
 *  Free Software Foundation; either version 2 of the License, or (at your
 *  option) any later version.
 *  
 *  Dex is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 *  Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */
package dex.compiler.parser;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.util.Collection;

import dex.compiler.model.base.Place;


/**
 * Input for the parser.
 * 
 * <p>This class uses a notion of an <i>input pointer</i> to perform
 * common operations on the input stream.  Some operations advance
 * the input pointer, while others do not.
 * 
 * <p>There are five basic operations provided by this class:
 * 
 * <ul>
 * <li>A <i>read</i> operation returns characters from the stream,
 * advancing the input pointer.</li>
 * <li>A <i>peek</i> operation returns characters from the stream
 * <i>without</i> advancing the input pointer.
 * <li>A <i>skip</i> operation advances the input pointer past any
 * whitespace characters.</li>
 * <li>A <i>test</i> operation reads characters from the stream,
 * comparing them to some expected input.  A test operation returns
 * true or false depending on whether or not the actual input matched
 * the expected input.  A test operation does <i>not</i> advance
 * the input pointer.</li>
 * <li>An <i>expect</i> operation reads characters from the string
 * and compares them to some expected input.  If the input is not
 * what's expected, an exception is raised.  Expect operations 
 * advance the input pointer.</li>
 * </ul>
 * 
 * 
 * <p>Chief among the responsibilities of this class is to keep track
 * of the current line and character number of the input.
 * 
 */
public class Input {
	

	/**
	 * The reader that produces characters.
	 */
	private PushbackReader reader;
	
	
	/**
	 * The name of the file where the input came from.
	 * May be null if the input isn't coming from a file.
	 */
	private String fileName;
	
	
	/**
	 * The current line number of the input.
	 */
	private int line;
	
	
	/**
	 * The current character of the current line.
	 */
	private int character;
	
	
	/**
	 * A saved line number.  Referred to when an unread operation
	 * takes place.
	 */
	private int savedLine;
	
	
	/**
	 * A saved character number.  Referred to when an unread operation
	 * takes place.
	 */
	private int savedCharacter;
	

	/**
	 * Constructs a new <code>Input</code>. 
	 * 
	 * @param reader  the reader that will produce characters to parse
	 */
	public Input(Reader reader) {
		this(null, reader);
	}
	

	/**
	 * Constructs a new <code>Input</code> with the given file name.
	 * 
	 * @param fileName  the name of the file that's generating input
	 * @param reader    the reader that will produce characters to parse
	 */
	public Input(String fileName, Reader reader) {
		super();
		this.reader = new PushbackReader(reader, 256);
		this.fileName = fileName;
		this.line = 1;
		this.character = 1;
		this.savedLine = 1;
		this.savedCharacter = 1;
	}

	
	/**
	 * Returns the current place.
	 * 
	 * @return  the current place
	 */
	public Place getPlace() {
		return new Place(fileName, line, character);
	}
	
	
	/**
	 * Saves the current line and character for future reference.
	 * Invoked in anticaption of an unread operation.
	 * This isn't stacked; only one line/character state is ever
	 * remembered.
	 */
	private void mark() {
		savedLine = line;
		savedCharacter = character;
	}
	
	
	/**
	 * Resets the current line and character from a previously
	 * marked state.
	 */
	private void reset() {
		if ((savedLine <= -1) || (savedCharacter <= 0)) {
			throw new AssertionError();
		}
		line = savedLine;
		character = savedCharacter;
	}
	
	
	/**
	 * Returns the next character in the input.
	 * For the sake of sanity, this method returns zero if the end-of-input
	 * has been reached.  That means that Dex source files can't contain the
	 * null character.  I'm okay with that.
	 * 
	 * @return  the next character in the input, or zero if the end-of-input
	 *   has been reached
	 */
	public char read() {
		if (line <= 0) {
			throw new AssertionError();
		}
		try {
			int ch = reader.read();
			if (ch < 0) {
				return 0;
			}
			if (ch == '\n') {
				line++;
				character = 1;
			} else {
				character++;
			}
			return (char)ch;
		} catch (IOException e) {
			throw new RuntimeException(e);
		} finally {
			if (line <= 0) {
				throw new AssertionError();
			}
		}
	}
	
	
	/**
	 * Unreads a character.  
	 * 
	 * @param ch  the character to unread
	 */
	private void unread(char ch) {
		try {
			reader.unread(ch);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
	
	
	/**
	 * Returns true if the next token in the input is one of given strings.
	 * 
	 * @param values  the values to test
	 * @param delims  the delimiters used to tokenize
	 * @return  true if the next token matches one of the values
	 */
	public String test(Collection<String> values, Breaks delims) {
		for (String v : values) {
			if (test(v, delims)) {
				return v;
			}
		}
		return null;
	}
	
	
	public String skipAndTest(Collection<String> values, Breaks delims) {
		skip();
		return test(values, delims);
	}
	
	
	/**
	 * Returns the next character in the input without advancing the input 
	 * pointer.
	 * 
	 * @return
	 */
	public char peek() {
		mark();
		char result = read();
		unread(result);
		reset();
		return result;
	}
	
	
	/**
	 * Tests if the next token in the input is the given string.
	 * 
	 * @param value  the string to test
	 * @param delims  the delimiters used to break tokens
	 * @return  true if the next token is the given value
	 */
	public boolean test(String value, Breaks delims) {
		if (value.length() > 32) {
			throw new IllegalArgumentException("String too long.");
		}
		mark();
		StringBuilder sb = new StringBuilder(value.length());
		for (int i = 0; i < value.length(); i++) {
			sb.append(read());
		}
		boolean result = sb.toString().equals(value);
		char delim = read();
		result = result && delims.isDelimiter(delim);
		unread(delim);
		for (int i = sb.length() - 1; i >= 0; i--) {
			unread(sb.charAt(i));
		}
		reset();
		return result;
	}

	
	/**
	 * Skips whitespace characters.
	 */
	public void skip() {
		skipAndPeek();
	}
	

	/**
	 * Skips whitespace characters and peeks the first non-whitespace character.
	 * The input pointer will advance past the whitespace characters, but not past
	 * the returned character.
	 * 
	 * <p>Note the returned value may be zero to indicate end-of-input.
	 * 
	 * @return  the next non-whitespace character 
	 */
	public char skipAndPeek() {
		mark();
		char ch;
		for (ch = read(); Character.isWhitespace(ch); ch = read()) {
			mark();
		}
		unread(ch);
		reset();
		return ch;
	}


	/**
	 * Skips whitespace characters and reads the first non-whitespace character.
	 * The input pointer will advance past both the whitespace characters and
	 * the returned character.
	 * 
	 * <p>Note the returned value may be zero to indicate end-of-input.
	 * 
	 * @return  the next non-whitespace character
	 */
	public char skipAndRead() {
		char ch = read();
		while (Character.isWhitespace(ch)) {
			ch = read();
		}
		return ch;
	}
	
	
	/**
	 * Skips whitespace characters then expects the given character.
	 * The input pointer will advance past both the whitespace characters
	 * and the expected character.  If the expected character does not
	 * occur immediately after the whitespace, a {@link ParseException}
	 * is thrown.
	 * 
	 * @param expected  the expected character
	 */
	public void skipAndExpect(char expected) {
		char ch = skipAndRead();
		if (ch != expected) {
			throw new ParseException(getPlace(), "Expected " + expected + " but got " + ch);
		}
	}
	
	
	/**
	 * Skips whitespace characters then expects the given string.
	 * The input point will advance past both the whitespace characters
	 * and the expected string.  If the expected string does not
	 * occur immediately after the whitespace, a {@link ParseException}
	 * is thrown.
	 * 
	 * @param expected  the string that should follow the whitespace
	 * @param delims    the delimiters used to break tokens
	 */
	public void skipAndExpect(String expected, Breaks delims) {
		skipAndPeek();
		expect(expected, delims);
	}

	
	/**
	 * Expects the given character.  The input pointer will advance
	 * past the next character in the stream.  If that character is
	 * not the given character, an exception is raised.
	 * 
	 * @param expected  the expected character
	 */
	public void expect(char expected) {
		char ch = read();
		if (ch != expected) {
			throw new ParseException(getPlace(), "Expected " + expected + " but got " + ch);
		}
	}
	
	
	/**
	 * Expects the given string.  The input pointer will advance
	 * past the string.  If the read string is not the same as the
	 * given string, an exception is raised.  
	 *
	 * @param value   the value to expect
	 * @param delims  the delimiters used to break tokens
	 */
	public void expect(String value, Breaks delims) {
		StringBuilder temp = new StringBuilder(value.length() + 1);
		for (int i = 0; i < value.length(); i++) {
			temp.append(read());
		}
		char delim = peek();
		if (!delims.isDelimiter(delim)) {
			temp.append(delim);
		}
		
		if (!temp.toString().equals(value)) {
			throw new ParseException(getPlace(), "Expected " + value + " but got " + temp + ".");
		}
	}


	/**
	 * Reads until the given terminator is detected.  The input pointer
	 * advances past characters until the terminator is discovered.
	 * The input pointer is also advanced past the terminator.
	 * The terminator is not included in the returned string.
	 * 
	 * @param terminator  the string that terminates the read
	 * @return  the read characters
	 */
	public String readUntil(String terminator) {
		StringBuilder sb = new StringBuilder();
		do {
			char ch = read();
			if (ch == 0) {
				throw new ParseException(getPlace(), "Unexpected end of input.");
			}
			sb.append(ch);
		} while (!endsWith(sb, terminator));
		
		// Delete the terminator
		int start = sb.length() - terminator.length();
		int end = sb.length();
		sb.delete(start, end);
		return sb.toString();
	}
	

	/**
	 * Returns true if the given buffer ends with the terminator.
	 * 
	 * @param sb  the buffer to compare
	 * @param terminator  the terminator to compare
	 * @return  true if the buffer ends with that terminator
	 */
	private static boolean endsWith(StringBuilder sb, String terminator) {
		if (sb.length() < terminator.length()) {
			return false;
		}
		int start = sb.length() - terminator.length();
		for (int i = 0; i < terminator.length(); i++) {
			if (sb.charAt(start + i) != terminator.charAt(i)) {
				return false;
			}
		}
		return true;
	}


}
